package com.briup.pts.aop;

import com.briup.pts.config.auth.SecurityUserContext;
import com.briup.pts.config.exception.ServiceException;
import com.briup.pts.domain.bean.Account;
import com.briup.pts.domain.bean.Log;
import com.briup.pts.service.LogService;
import com.briup.pts.utils.IpUtil;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import java.time.LocalDateTime;

/**
 * 编写日志的切面类，需要通过@Aspect注解标注这个一个切面
 */
@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class LogAspect {

    private final LogService logService;

    /*
    设置切面切入数据的切入点：切入点表示的就是需要被增强的方法
     在本切面中，表示的就是需要被记录日志的操作方法
     切入点表达式 execution()进行表示
     execution(* com.briup.pts..controller.*.*(..))表示匹配 com.briup.pts包下的任意子包中
     的controller包下的所有Controller类中的所有方法，无论该方法的参数列表以及返回值类型是什么
     但是，需要注意不包含私有方法，因为私有方法是无法被子类继承并且重写的；
     @annotation() 可以限制切入点方法必须标注了你指定的注解
     */
    @Pointcut("execution(* com.briup.pts..controller.*.*(..)) && " +
            "@annotation(com.briup.pts.aop.Logger)")
    public void pointCut() {
    }


    /*
    对目标方法的增强逻辑，我们可以写入到 通知中：
    AOP通知包含：
        前置通知：指的是在目标方法执行之前执行的 增强逻辑，叫做前置通知
        后置通知：指的是在目标方法执行完毕之后执行的 增强逻辑，叫做后置通知；
                注意，后置通知是无论该方法是否出现异常都会执行的通知
        返回后通知：指的是当目标方法正常执行完毕，并且返回了返回值之后执行的增强逻辑，叫做返回后通知，
                注意，返回后通知与异常后通知互斥
        异常后通知：指的是当目标方法运行时出现异常后会执行的增强逻辑，叫做返回后通知
        环绕通知：指的是在目标方法执行前后都编写增强代码，由程序员控制什么时候执行目标方法的通知
     */
    @Around("pointCut()")
    public Object around(ProceedingJoinPoint pjp){
        // 创建日志信息，最终将日志信息写入到数据库中
        Log sysLog = new Log();
        Account user = SecurityUserContext.currentUser();
        // 设置当前操作相关用户的信息，即设置当前登录用户的信息
        sysLog.setLogUsername(user == null ? "未知" : user.getUsername());
        sysLog.setLogRealname(user == null ? "未知" : user.getRealname());

        // 解析请求获取请求相关的信息
        parseRequest(sysLog);

        // 设置操作功能描述，需要获取操作方法上标注的@Logger注解中维护的value属性值
        MethodSignature signature = (MethodSignature) pjp.getSignature();
        Logger logger = signature.getMethod().getAnnotation(Logger.class);
        sysLog.setLogDescription(logger.value());

        Object obj;
        try {
            obj = pjp.proceed();
            sysLog.setLogResult("操作成功");
        } catch (Throwable e) {
            // 出现异常了，将错误进行转发
            sysLog.setLogResult("操作失败，原因:"+e.getMessage());
            throw new ServiceException(e.getMessage());
        }finally {
            // 无论功能操作是否执行成功，都需要写入到对应的日志表中
            sysLog.setLogTime(LocalDateTime.now());
            logService.addLog(sysLog);
        }

        return obj;
    }

    private void parseRequest(Log sysLog) {
        // 从应用上下文中获取对应请求
        ServletRequestAttributes requestAttributes
                = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        assert requestAttributes != null;
        HttpServletRequest request = requestAttributes.getRequest();

        // 设置请求方式和请求路径
        sysLog.setLogRequestMethod(request.getMethod());
        sysLog.setLogRequestUri(request.getRequestURI());

        String ip = IpUtil.getRealIp(request);
        sysLog.setLogHost(ip);
        log.info("本次发生请求的ip为:" + ip);
    }
}
